/*
 * Copyright Aristool AG Switzerland
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package org.opprua.tidy.gens;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.opprua.tidy.util.CodePrint;
import org.opprua.tidy.util.FileUtil;

/**
 * TODO: add Java doc
 * 
 * Prepared for first commit: 15.04.2007
 * 
 * @author Lin Li
 */
public class GClass extends Gtor {

  private String packageName = "";
  private String className = "";
  private String extendsClass = "";
  private String modifiers = "";
  private String implementsClasses = "";
  private List<String> imports = new ArrayList<String>();
  private List<GMethod> methods = new ArrayList<GMethod>();
  private List<GVar> fields = new ArrayList<GVar>();
  private List<GClass> classes = new ArrayList<GClass>();
  private List<Object> statics = new ArrayList<Object>();

  /**
   * <p>
   * Constructor for a generator of a class or interface with the name given.
   * </p>
   * 
   * @param name
   *          name of the class to be generated.
   */
  public GClass(String name) {
    super();
    className = name;
  }

  /**
   * <p>
   * Sets the modifiers for the class or interface to be generated For example,
   * "public abstract class" or "Interface". If none is set, the standard
   * modifier will be "public class".
   * </p>
   * 
   * @param s
   *          the modifiers.
   * @return this, for chained call.
   */
  public GClass setModifiers(String s) {
    modifiers = s;
    return this;
  }

  /**
   * <p>
   * Sets the class/interface list for the extends keyword. The string will be
   * copied into the right place as is.
   * </p>
   * 
   * @param s
   *          the class/interface list.
   * @return this, for chained calls
   */
  public GClass setExtends(String s) {
    extendsClass = s;
    return this;
  }

  /**
   * <p>
   * Sets the interface list for the implements keyword. The string will be
   * copied into the right place as is.
   * </p>
   * 
   * @param s
   *          the interface list.
   * @return this, for chained calls
   */
  public GClass setImplements(String s) {
    implementsClasses = s;
    return this;
  }

  /**
   * <p>
   * Set the name of the package for this class/interface. If you use
   * dump(String) to generate the file, the file will be generated at the place
   * corresponding to its package.
   * </p>
   * 
   * @param n
   *          the package name
   * @return this, for chained calls
   */
  public GClass setPackage(String n) {
    packageName = n;
    return this;
  }

  /**
   * <p>
   * Add an import element to the class. The name is taken as is for the import
   * statement.
   * </p>
   * 
   * @param n
   *          name of the imported element
   * @return this, for chained calls
   */
  public GClass addImport(String n) {
    imports.add(n);
    return this;
  }

  /**
   * <p>
   * Add a method to be generated by the method generator. The method gen is
   * "live" so that its building can start before this call and continue after
   * this call. The dump() to this generator ends the building of that method.
   * </p>
   * 
   * @param m
   *          the method generator
   * @return this, for chained calls.
   */
  public GClass addMethod(GMethod m) {
    methods.add(m);
    return this;
  }

  /**
   * <p>
   * Add a field generated, or to be generated, by f.
   * </p>
   * 
   * @param f
   *          the generator for the field
   * @return this, for chained calls.
   */
  public GClass addField(GVar f) {
    fields.add(f);
    return this;
  }

  /**
   * <p>
   * Add a inner class generated, or to be generated, by f.
   * </p>
   * 
   * @param f
   *          the generator for the inner class
   * @return this, for chained calls.
   */
  public GClass addInnerClass(GClass f) {
    classes.add(f);
    return this;
  }

  /**
   * <p>
   * Add class static code statement to the class. All static code for the class
   * will be grouped in one block and inserted after all the variables are
   * declared.
   * </p>
   * 
   * @param s
   *          the string as a statement to be put into the class static code
   *          block
   * @return this, for chained calls.
   */
  public GClass addStatics(String s) {
    statics.add(s);
    return this;
  }

  /**
   * <p>
   * Add class static code statement to the class. All static code for the class
   * will be grouped in one block and inserted after all the variables are
   * declared.
   * </p>
   * 
   * @param s
   *          the generator of a statement to be put into the class static code
   *          block
   * @return this, for chained calls.
   */
  public GClass addStatics(GState s) {
    statics.add(s);
    return this;
  }

  /**
   * <p>
   * The name of the class/interface been generated.
   * </p>
   * 
   * @return the name of the class
   */
  public String getClassName() {
    return className;
  }

  /**
   * <p>
   * Convenience function to get a field in one call.
   * </p>
   * 
   * @see GVar
   */
  public void addField(String modifiers, String type, String name,
              String initializer) {
    addField(new GVar(modifiers, type, name, initializer));
  }

  /**
   * <p>
   * Convenience function to get method generator already added to the class.
   * </p>
   * 
   * @see GMethod
   */
  public GMethod newMethod(String modifiers, String name, String returnType) {
    GMethod m = new GMethod(modifiers, name, returnType);
    addMethod(m);
    return m;
  }

  /**
   * <p>
   * Write the code into the writer.
   * </p>
   */
  public void dump(CodePrint w) {
    if (packageName != null && packageName.trim().length() > 0) {
      w.println("package " + packageName + ";");
      w.println();
    }

    for (Iterator<String> i = imports.iterator(); i.hasNext();) {
      w.println("import " + i.next() + ";");
    }
    w.println();

    javadoc(w);
    printAnnotation(w);

    w.print(test(modifiers) ? modifiers + " " : "public class ");
    w.print(className);
    w.printPadded(test(extendsClass) ? "extends " + extendsClass : "");
    w.printPadded(test(implementsClasses) ? "implements " + implementsClasses
                : "");
    w.println(" {");

    w.einruecken();

    w.println();

    for (Iterator<GClass> i = classes.iterator(); i.hasNext();) {
      i.next().dump(w);
    }

    for (Iterator<GVar> i = fields.iterator(); i.hasNext();) {
      i.next().dump(w);
    }

    if (statics.size() > 0) {
      w.println("static {");
      w.einruecken();
      for (Iterator<Object> i = statics.iterator(); i.hasNext();) {
        Object o = i.next();
        if (o instanceof String) {
          w.println((String) o);
        } else if (o instanceof GState) {
          ((GState) o).dump(w);
        }
      }
      w.ausruecken();
      w.println("}");
    }

    w.println();
    for (Iterator<GMethod> i = methods.iterator(); i.hasNext();) {
      ((GMethod) i.next()).dump(w);
    }

    w.ausruecken();

    w.println("}");
  }

  /**
   * <p>
   * Write the code into a file. The directory given should be the root
   * directory of the java code. This method will create all the directories
   * necessary leading to the right place under that root directory and create
   * the file to contain the generated code there.
   * </p>
   * 
   * @param rootDir
   *          the root directory for the generated java code.
   */
  public boolean dump(String rootDir) {    
    String path = rootDir + packageName.replace('.', '/') + "/" + className
                + ".java";
    try {
      StringWriter s = new StringWriter();
      CodePrint c = CodePrint.create(new PrintWriter(s));
      dump(c);
      c.close();

      File f = new File(path);
      if (FileUtil.exists(path)) {
        BufferedReader o = new BufferedReader(new FileReader(f));
        BufferedReader n = new BufferedReader(new StringReader(s.toString()));

        for (;;) {
          String os = nextLine(o);
          String ns = nextLine(n);
          if (os == null && ns == null) {
            o.close();
            return false;
          } else if (os == null || ns == null || !os.trim().equals(ns.trim())) {
            o.close();
            break;
          }
        }
      }
      PrintWriter p = FileUtil.getWriter(path);
      p.print(s.toString());
      p.close();
      return true;
    } catch (Throwable e) {
      throw new Error("Failed to dump " + className + " to " + path, e);
    }
  }

  public int numberOfFields() {
    return fields.size();
  }

  public GVar getField(int i) {
    return fields.get(i);
  }

  private String nextLine(BufferedReader b) throws Exception {
    while (b.ready()) {
      String s = b.readLine();
      if (s == null) {
        return null;
      }
      if (s.trim().length() > 0) {
        return s;
      }
    }
    return null;
  }
}
